package anonymouschat.service;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;

import anonymouschat.Identity;
import anonymouschat.messages.AESkeyMessage;
import anonymouschat.messages.Message;
import anonymouschat.messages.ServerAnnounceMessage;
import anonymouschat.messages.TextMessage;
import anonymouschat.messages.clientrequests.ChatroomChangeRequest;
import anonymouschat.messages.clientrequests.UserLeaveRequest;
import anonymouschat.messages.clientrequests.UserLoginRequest;
import anonymouschat.messages.clientrequests.encryptedMessageWrapperMessage;
import anonymouschat.messages.serverresponses.ChatroomChangeResponse;
import anonymouschat.messages.serverresponses.ChatroomMemberResponse;
import anonymouschat.utils.ConstantVariables;
import anonymouschat.utils.ListOfServers;
import anonymouschat.utils.serverConnectionEntry;


/**
 * Connection coming into a server (send messages TO a server with an instance
 * of this class)
 * 
 */
public class InConnectionThread extends Thread
{

	private Socket clientSocket;

	private ObjectInputStream in = null;

	private ObjectOutputStream out = null;

	private String serverName;

	private SecretKey SymmetricKey;

	private Cipher decryptEngine;
	
	private SecureRandom rand = new SecureRandom();
	
	private ArrayList<Long> usedConnectionNumbers = new ArrayList<Long>();
	
	private ListOfServers availableServers = new ListOfServers();
	
	private volatile ConcurrentMap<String, serverConnectionEntry> connections = new ConcurrentHashMap<String, serverConnectionEntry>();

	/**
	 * Server will read objects from the socket clientSocket, with the
	 * ObjectInputStream inputStream
	 * 
	 * @param clientSocketIn
	 * @param inputStream
	 */
	InConnectionThread( Socket clientSocketIn, ObjectInputStream inputStream, ObjectOutputStream outStream ) {
		clientSocket = clientSocketIn;
		in = inputStream;
		 out = outStream;
		this.serverName = Server.getServerName();
	}

	@Override
	public void run() {
		// fix this
		Object message = null;
		Object unwrappedMessage = null;
		/*
		 * try { in = new ObjectInputStream(clientSocket.getInputStream()); }
		 * catch (IOException e) { e.printStackTrace(); }
		 */
		Long incommingConnectionNumber = null;
		while( clientSocket.isConnected() ) {

			
			try {
				message = (Object) in.readObject();
			} catch( IOException e ) {
				// System.err.println("ERROR on SERVER when reading object from ObjectInputStream: IO Error, attempting to continue");
				// e.printStackTrace();
				continue;
			} catch( ClassNotFoundException e ) {
				System.err
				.println( "ERROR on SERVER when reading object from ObjectInputStream: Class not found" );
				e.printStackTrace();
			}

			if (message instanceof encryptedMessageWrapperMessage)
			{
				
					encryptedMessageWrapperMessage encMsg = (encryptedMessageWrapperMessage)message;
					incommingConnectionNumber = encMsg.getConnectionNumber();
					//if (encMsg.isAsymmetricEncrypted())
					//	unwrappedMessage = Server.Identity.unwrapEncryptionLayer(encMsg);
					//else
					//	unwrappedMessage = unwrapSymmetricEncryptionLayer(encMsg);
					
					ByteArrayInputStream bStream = new ByteArrayInputStream(encMsg.getencryptedMessage());;
					ObjectInputStream iStream;
					try {
						iStream = new ObjectInputStream(bStream);
					
					unwrappedMessage =  iStream.readObject();
					} catch (IOException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					} catch (ClassNotFoundException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
			}

			System.out.println( "Received message: " + unwrappedMessage.toString() );

			if( unwrappedMessage instanceof TextMessage ) {
				try {
					HandleTextMessage( (TextMessage) unwrappedMessage );
				} catch( Exception e ) {
					e.printStackTrace();
				}
			//} else if( unwrappedMessage instanceof ChatroomChangeRequest ) {
				//HandleChatroomChangeRequest( (ChatroomChangeRequest) unwrappedMessage );
			} else if( unwrappedMessage instanceof UserLeaveRequest ) {
				HandleUserLeaveRequest( (UserLeaveRequest) unwrappedMessage );
			} else if( unwrappedMessage instanceof encryptedMessageWrapperMessage) {
				HandleEncryptedMessage( (encryptedMessageWrapperMessage) unwrappedMessage, incommingConnectionNumber);
			} else if( unwrappedMessage instanceof UserLoginRequest ) {
				HandleUserLoginRequest( (UserLoginRequest) unwrappedMessage);
			} else if( unwrappedMessage instanceof ServerAnnounceMessage ) {
				HandleServerAnnounceMessage( (ServerAnnounceMessage) unwrappedMessage);
			} else if( unwrappedMessage instanceof AESkeyMessage ) {
				try {
					HandleAESkeyMessage( (AESkeyMessage) unwrappedMessage);
				} catch (NoSuchAlgorithmException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				} catch (NoSuchPaddingException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
			else {
			
			System.err.println( "ERROR: Unknown message received from ip:"
					+ clientSocket.getInetAddress().toString() + ":"
					+ clientSocket.getPort() );
			}
		}
	}



	private Object unwrapSymmetricEncryptionLayer(
			encryptedMessageWrapperMessage encMsg) throws Exception {
		// TODO uncomment symmetric key for encryption
		//if (SymmetricKey == null)
		//	throw new Exception("Symmetric key has not been recieved for this connection");

		try {
			decryptEngine = Cipher.getInstance(ConstantVariables.SYMMETRIC_CIPHER_TYPE);
			decryptEngine.init(Cipher.DECRYPT_MODE, SymmetricKey, encMsg.getIV());
			//TODO uncomment the following line to decrypt instead of the one below it
			//byte[] unwrapped = decryptEngine.doFinal(encMsg.getencryptedMessage());
			byte[] unwrapped = encMsg.getencryptedMessage();
			ByteArrayInputStream bStream = new ByteArrayInputStream(unwrapped);
			ObjectInputStream iStream = new ObjectInputStream(bStream);
			return iStream.readObject();

		} catch (Exception e)
		{
			throw new Exception("Error encountered while decrypting encryptedMessage using Symmetric decryption" + e.getLocalizedMessage());
		}
	}

	private void HandleAESkeyMessage(AESkeyMessage unwrappedMessage) throws NoSuchAlgorithmException, NoSuchPaddingException {
		// TODO Auto-generated method stub
		System.out.println("Server " + serverName + " recieved AES KEY");
		SymmetricKey = unwrappedMessage.SharedAESKey;
	}


	private void HandleServerAnnounceMessage(
			ServerAnnounceMessage unwrappedMessage) {
		System.out.println("Server " + serverName + " recieved SERVER ANNOUNCE MESSAGE");
		// TODO Auto-generated method stub

	}

	private void HandleUserLoginRequest(UserLoginRequest unwrappedMessage) {
		// TODO Auto-generated method stub
		System.out.println("Server " + serverName + " recieved USER LOGIN REQUEST MESSAGE");
		//Server.currentConnections.put(unwrappedMessage.getUserID(), out);
	}

	/**
	 * Handles messages that are encrypted and still need to be forwarded
	 * @param unwrappedMessage The message that has already been decrypted at this server
	 */
	private void HandleEncryptedMessage(
			encryptedMessageWrapperMessage unwrappedMessage, Long incommingConnectionNumber) {
		// TODO Auto-generated method stub

		System.out.println("Server " + serverName + " recieved ENCRYPTED MESSAGE");
		
		try {
			if (unwrappedMessage.getConnectionNumber() != null)
			{
				unwrappedMessage.setConnectionNumber(getUniqueNumber());
				addConnectionInfo(unwrappedMessage.getConnectionNumber(), incommingConnectionNumber, out);
			}
			
			OutConnection o = getConnection(unwrappedMessage.getNextHop());
			o.sendMessage(unwrappedMessage);
			
		} catch (Exception e) {
			System.err.println("Error occured while forwarding encrypted message to: " + unwrappedMessage.getNextHop());
			e.printStackTrace();
		} 
		//TODO Send message to server specified in unwrappedMessage's next hop field
	}

	private void addConnectionInfo(Long connectionNumber,
			Long incommingConnectionNumber, ObjectOutputStream out2) {
		
		serverConnectionEntry s = new serverConnectionEntry();
		s.setServerConnectionNumber(connectionNumber);
		s.setSocketConnection(out2);
		connections.put(""+incommingConnectionNumber, s);
	}

	private long getUniqueNumber() {
      Long result;
		do
       {
    	  result = rand.nextLong();
       } while (usedConnectionNumbers.contains(result));
		usedConnectionNumbers.add(result);
		return result;
	}

	/**
	 * User leave.
	 * 
	 * remove this user from chatroom member list.
	 * 
	 * send ChatroomMemberResponse to everyone in the chatroom.
	 * 
	 * @param message
	 */
	private void HandleUserLeaveRequest( UserLeaveRequest message ) {
		System.out.println("Server " + serverName + " recieved USER LEAVE REQUEST");

		List<String> memberList = Server.chatroomMemberList.get( message
				.getChatroomName() );
		if( memberList == null ) {
			System.out
			.println( "ERROR!! user asks to leave a non-existing chat room" );
		} else {
			if( !memberList.remove( message.getUserID() ) ) {
				System.out
				.println( "ERROR!! user asks to leave a chatroom where they are not in" );
			}
		}
		for( int i = 0; i < memberList.size(); i++ ) {
			try {
				ChatroomMemberResponse resp = new ChatroomMemberResponse(
						message.getChatroomName(), memberList );
				Server.currentConnections.get( memberList.get( i ) )
				.sendMessage( resp );
			} catch( IOException e ) {
				System.err.println( "Unable to tell user '"
						+ memberList.get( i ) + "' the updated user list" );
				e.printStackTrace();
			}
		}
	}

	
	private OutConnection getConnection(String destinationName) throws Exception
	{
		OutConnection con = null;
		if (!Server.currentConnections.containsKey(destinationName))
		{
			try {
				    serverEntry dest = availableServers.getByName(destinationName);
					// create new server to server connection
					Socket s2sSocket = new Socket(dest.serverIP, dest.serverPort ); 
					ServerAnnounceMessage me = new ServerAnnounceMessage( serverName );
					con = new OutConnection( s2sSocket, dest.serverName );
					 ObjectOutputStream out2 = con.getOutputStream();

					 //Write announce message to server
					 try {
						 encryptedMessageWrapperMessage enc = new encryptedMessageWrapperMessage(availableServers.getByName(destinationName).getAsymmetricCipherEngine(), me, true);
					  out2.writeObject( enc );
					  out2.flush();
					 } catch( IOException e1 ) {
					     throw new Exception("Erorr encountered while sending announce message to server: " + destinationName);
					 }
					 
					Server.currentConnections.put( dest.serverName, con );
					InConnectionThread newInConnection = new InConnectionThread(s2sSocket, new ObjectInputStream(s2sSocket.getInputStream() ), out2 );
					newInConnection.start();
				} catch (Exception e)
				{
					throw new Exception("Unable to connect to server " + destinationName);
				}
			}
			if (Server.currentConnections.containsKey(destinationName))
			   return Server.currentConnections.get(destinationName);
			else 
				throw new Exception("Unable to connect to server " + destinationName);
		}

		/**
		 * 
		 * @param message
		 */
		private void HandleTextMessage( TextMessage message ) {
			// check if destination is local or another server
			System.out.println("Server " + serverName + " recieved TEXT MESSAGE");


			// Check to see if talking to one person or a chatroom.
			String dest = null;
			if( message.destinationUser != null ) {
				dest = message.destinationUser;
			} else if( message.destinationChatroom != null ) {
				System.err.println( "Routing is not functioning for chatrooms at this time..." );
				dest = message.destinationChatroom;
			} else {
				System.err.println( "Error in InConnectionThread: unable to handle text message because both destination user, and chatroom are null." );
			}

			try {
				if (Server.currentConnections.containsKey(dest)) //Recipient also published here
				{
					//Connections contains links from username/unique server number to tcp connection
					//TODO more needs to be done here
				    connections.get(dest).getSocketConnection().writeObject(message);
				} else {
					//TODO Put Jason's code here to determine server to send to
					//getConnection(server here).sendMessage(message);
				}
			} catch (Exception e) {
				System.err.println("Error handling text message: " + e.getLocalizedMessage());
				e.printStackTrace();
			} 	
		}

		public void serverSendMessage(Message msg, String destination)
		{
			try {
				encryptedMessageWrapperMessage enc = new encryptedMessageWrapperMessage(availableServers.getByName(destination).getAsymmetricCipherEngine(), msg, true);
				getConnection(destination).sendMessage(enc);
			} catch (Exception e) {
				// TODO Auto-generated catch block
				System.err.println("Error encountered when sending server->server message " + serverName + " -> " + destination);
				e.printStackTrace();
			} 
		}
		
	}
